<?php
/*-------------------------------------------------------------------------\
|
|  ========================================================
|  Fusion Registry GPL
|  Copyright (C) 2007 Fusion Scripts
|
|  This program is free software; you can redistribute it and/or
|  modify it under the terms of the GNU General Public License
|  as published by the Free Software Foundation; either version 2
|  of the License, or (at your option) any later version.
|
|  This program is distributed in the hope that it will be useful,
|  but WITHOUT ANY WARRANTY; without even the implied warranty of
|  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
|  GNU General Public License for more details.
|
|  You should have received a copy of the GNU General Public License
|  along with this program; if not, write to the Free Software
|  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
|  ========================================================
|
|  Email: info@fusionscripts.com
|  Web-Site: http://www.fusionscripts.com/
|
|  $Id: session.php 4 2008-08-03 01:43:03Z registry.fusion $
|
\-------------------------------------------------------------------------*/

/**
 * Custom database session handler
 *
 * Mimics native php session handler, but does not use any of the native
 * php functions.
 *
 * @package Fusion Registry
 * @subpackage lib
 * @version $Id: session.php 4 2008-08-03 01:43:03Z registry.fusion $
 */

if ( ! defined( 'IN_IPB' ) )
{
    print "<h1>Incorrect access</h1>You cannot access this file directly. If you have recently upgraded, make sure you upgraded all the relevant files.";
    exit();
}

/**
 * Class 'registry_session' is a custom database session handler
 *
 * Although this does not use any of the native php session handler functions, 
 * it does mimic the interface as closely as possible. This does not use php
 * session handler functions because there were compatability problems with
 * IE and the IPB cache limiter, which is out of scope of the Fusion Registry.
 *
 * @package Fusion Registry
 * @subpackage lib
 */
class registry_session
{
    var $ipsclass;

    /**
     * Cutoff time for inactive sessions to be removed
     *
     * A session with no actively for at least this many seconds will be
     * removed when garbage collection runs. Note that garbage collection may
     * not run on every execution, it is controlled by the same formula as php's
     * built in session handler for determining if garbage collection should run.
     * If the admin has configured the task in their ACP to clean up Fusion Registry
     * sessions that will override any probability formulas and guarantee
     * inactive session removal defined by the interval they configure that task
     * to run in their ACP task manager.
     *
     * @var integer Defines cutoff time in seconds for an inactive session
     */
    var $gc_maxlifetime = 1800;

    /**
     * Used in the formula to decide whether garbage collection should be run
     *
     * Setting to a value < 1 will prevent garbage collection from running, and
     * a value 1 or higher will increase probability that garbage collection will
     * run.
     *
     * @var integer Probability of garbage collection being run
     */
    var $gc_probability = 1;

    /**#@+ 
     * Private class variables, should not be accessed externally
     * @var string
     * @access private
     */
    var $_bad_session_id = NULL;
    var $_ip_address     = NULL;
    var $_md5_user_agent = NULL;
    var $_session_id     = NULL;
    var $_time_now       = 0;
    var $_time_updated   = 0;
    /**#@-*/

    /**#@+ 
     * Private class variables, should not be accessed externally
     * @var array
     * @access private
     */
    var $_cookie       = array();
    var $_session_data = array();
    /**#@-*/


    /**
     * Constructor, auto-initiated.
     *
     * Intialization of class wide data, and kickoff the r_start_session().
     * This function should have minimal procedural code.
     *
     * @global object
     * @return boolean TRUE
     */
    function auto_run()
    {
        // Store some class variables
        $_SERVER['HTTP_USER_AGENT'] = $this->ipsclass->clean_value($_SERVER['HTTP_USER_AGENT']);

        $this->_md5_user_agent = md5( substr($_SERVER['HTTP_USER_AGENT'], 0, 50) );

        $this->_ip_address = $_SERVER['REMOTE_ADDR'];

        $this->_time_now = time();

        // Grab the cookie if it's there
        $this->_cookie['REGISTRY_SID'] = $this->ipsclass->my_getcookie('REGISTRY_SID');

        // Start our custom session
        $this->r_start_session();

        return TRUE;
    }


    /**
     * Create new session and store to DB
     *
     * We will pre-authorise this new session since we can assume it is secure,
     * because we are generating a fresh new session id and have already
     * grabbed their user agent.
     */
    function r_create_session()
    {
        // Create our session id
        $this->_session_id = $this->r_session_create_id();

        // Setup our session validation
        $this->set_session_var( array( 'initiated' => true,
                                       'HTTP_USER_AGENT' => $this->_md5_user_agent,
                              )      );
    }


    /**
     * Remove session from DB and class
     *
     * Instead of passing in the session id as a param, it expects to find
     * a session id in the {@link registry_session::_session_id} class variable.
     * If no value is present in the {@link registry_session::_session_id} class
     * variable it will not execute the cleanup of anything, but it will still
     * return TRUE. Also if it finds anything in the {@link registry_session::bad_session_id}
     * class variable it will append it to the SQL query to be deleted as well.
     *
     * @return boolean TRUE
     */
    function r_destroy_session()
    {
        $query_extra = '';
        if ( !empty($this->_session_id) )
        {
            // If we have a bad session marked, remove it also
            if ( !empty($this->_bad_session_id) )
            {
                $query_extra = " OR ses_id='".$this->_bad_session_id."' ";
            }

            // Remove it from the database
            $this->ipsclass->DB->simple_construct( array( 'delete' => 'registry_sessions', 'where' => "ses_id='".$this->_session_id."'".$query_extra ) );
            $this->ipsclass->DB->simple_exec();

            // Zero out our session variables
            $this->_session_id     = NULL;
            $this->_bad_session_id = NULL;
            $this->_session_data   = array();
        }

        return TRUE;
    }


    /**
     * Delete old sessions in the DB
     *
     * The cutoff session time is defined by the {@link registry_session::gc_maxlifetime}
     * class variable.
     */
    function r_garbage_collection()
    {
        // Calculate the cutoff time
        $date = ($this->_time_now - $this->gc_maxlifetime);

        // Bye-bye old sessions!
        $this->ipsclass->DB->simple_exec_query( array( 'delete' => 'registry_sessions',  'where'  => "ses_time < $date" ) );
    }


    /**
     * Start session with validation, maybe run garbage collection
     *
     * This function contains the heart of the session security. See
     * {@link http://phpsec.org} for details about the security implementations
     * here. The one exception to the security rules is that this function
     * will abide to the admin's wishes to match against browers or not. This
     * is less secure, but that warning is present in the ACP for this IPB setting.
     *
     * @return boolean TRUE
     */
    function r_start_session()
    {
        if ( $this->_cookie['REGISTRY_SID'] )
        {
            $this->r_read_session();

            // Protect against session fixation
            if ( $this->get_session_var('initiated') != true )
            {
                $this->r_session_regenerate_id();
            }

            // Protect against session hijacking.
            // Although less secure we will abide by the Admin's wishes to match
            //  against browsers or not.
            if ( $this->ipsclass->vars['match_browser'] == 1 )
            {
                if ( $this->get_session_var('HTTP_USER_AGENT') )
                {
                    if ( $this->get_session_var('HTTP_USER_AGENT') != $this->_md5_user_agent )
                    {
                        // Something is fishy, start them off fresh
                        $this->r_session_destroy();

                        // Kill stored cookie to prevent infinite loop
                        unset($this->_cookie['REGISTRY_SID']);

                        $this->r_start_session();
                    }
                }
                else
                {
                    $this->set_session_var( array( 'HTTP_USER_AGENT' => $this->_md5_user_agent ) );
                }
            }
        }
        else
        {
            $this->r_create_session();
        }

        // Update the session time, this will keep their sessions alive
        //  while they use the garage.
        $this->update_session_time();

        // Set session cookie
        $this->ipsclass->my_setcookie("REGISTRY_SID", $this->_session_id, -1);

        // Should we run garbage collection?
        if ( $this->gc_probability > 0 )
        {
            $randmax = getrandmax();
            $nrand = (int)(100 * mt_rand() / $randmax);

            if ( $nrand < $this->gc_probability )
            {
                $this->r_garbage_collection();
            }
        }

        return TRUE;
    }


    /**
     * Read session data from database
     *
     * This function is in charge of retrieval and unserialization of the
     * session data stored in the database, it should do minimal security
     * checks, the {@link r_start_session} function is in charge of security.
     *
     * @return boolean TRUE
     */
    function r_read_session()
    {
        $result = array();

        // Make sure we have a clean session_id value
        $this->ipsclass->session_id = preg_replace("/([^a-zA-Z0-9])/", "", $this->_cookie['REGISTRY_SID']);
        
        if ( $this->ipsclass->session_id )
        {
            $this->ipsclass->DB->simple_construct( array( 'select' => 'ses_id, ses_time, ses_start, ses_value',
                                                          'from'   => 'registry_sessions',
                                                          'where'  => "ses_id='".$this->ipsclass->session_id."' AND ses_ip_address='".$this->_ip_address."'"
                                                 )      );
                
            $this->ipsclass->DB->simple_exec();

            if ( $this->ipsclass->DB->get_num_rows() != 1 )
            {
                // Mark this session_id bad because there is no session,
                //  or there are more than one
                $this->_bad_session_id = $this->ipsclass->session_id;
                $this->_session_id     = 0;
                $this->_session_data   = array();
            }
            else
            {
                $result = $this->ipsclass->DB->fetch_row();

                // Make sure it is a valid data set from the DB
                if ( $result['ses_id'] == "" )
                {
                    // Mark this session_id bad because we didn't get a valid
                    //  data set from the DB
                    $this->_bad_session_id = $this->ipsclass->session_id;
                    $this->_session_id     = 0;
                    $this->_session_data   = array();
                    unset($result);
                }
                else
                {
                    // Horray, sessionize me!
                    $this->_session_id = $result['ses_id'];

                    // Bring our session value back to life
                    $this->_session_data = unserialize( $result['ses_value'] );
                    unset($result);
                }
            }
        }
        else
        {
            // Mark this session_id bad because the session_id isn't valid
            $this->_bad_session_id = $this->_cookie['REGISTRY_SID'];
            $this->_session_id     = 0;
            $this->_session_data   = array();
        }

        return TRUE;
    }


    /**
     * Create unique session id
     *
     * @return string Unique string used to define session id
     */
    function r_session_create_id()
    {
        return md5( uniqid(microtime()) );
    }

    
    /**
     * Create a new session id, preserving current session data
     *
     * This is called for the security measure against session fixation.
     *
     * @see r_start_session()
     * @return boolean TRUE
     */
    function r_session_regenerate_id()
    {
        // Save current session data
        $this->ipsclass->session_save = $this->get_session_var();

        // Destroy current session
        $this->r_destroy_session();

        // Generate our new session_id
        $this->_session_id = $this->r_session_create_id();

        // Restore session data, but first set the initiated flag
        $this->ipsclass->session_save['initiated'] = true;

        $this->set_session_var($this->ipsclass->session_save);

        return TRUE;
    }


    /**
     * Write current session data to database
     *
     * This function is in charge of serialization and storage of the
     * session data to the database. If the {@link registry_session::_session_id}
     * class variable is empty this will not store any data, but will still
     * return TRUE.
     *
     * @return boolean TRUE
     */
    function r_write_session()
    {
        $session_packed = '';

        if ( !empty($this->_session_id) )
        {
            // Prepare our data for storage
            $session_packed = serialize( $this->get_session_var() );

            // See if we need to do an UPDATE or INSERT
            $this->ipsclass->DB->simple_construct( array( 'select' => '*',
                                                          'from'   => 'registry_sessions',
                                                          'where'  => "ses_id='".$this->_session_id."'",
                                                 )      );

            $this->ipsclass->DB->simple_exec();

            if ( $this->ipsclass->DB->get_num_rows() > 0 )
            {
                // Update the current session entry
                $this->ipsclass->DB->simple_construct( array( 'update' => 'registry_sessions',
                                                              'set'    => "ses_value='$session_packed', ses_time='".$this->_time_now."'",
                                                              'where'  => "ses_id='".$this->_session_id."'",
                                                     )      );

                $this->ipsclass->DB->simple_exec();
            }
            else
            {
                // Insert a new session entry
                $this->ipsclass->DB->do_insert( 'registry_sessions', array( 'ses_id'         => $this->_session_id,
                                                                            'ses_value'      => $session_packed,
                                                                            'ses_time'       => $this->_time_now,
                                                                            'ses_start'      => $this->_time_now,
                                                                            'ses_ip_address' => $this->_ip_address ) );
            }

            // Flag that we have already updated the ses_time to save
            //  us an extra SQL query later.
            $this->_time_updated = 1;
        }

        return TRUE;
    }


    /**
     * External method call to get session id
     *
     * Always use this function call to access the session id in case the
     * internal class variable name ever changes.
     *
     * @return string Current session id
     */
    function get_session_id()
    {
        return $this->_session_id;
    }


    /**
     * External method call to get session variable
     *
     * Always use this function call to access session data in case the
     * internal class variable or structure ever changes.
     *
     * @param string $var Optional specific session variable to retrieve
     * @return string|array Specific requested session variable or entire session data array
     */
    function get_session_var($var='')
    {
        if ( !empty($this->_session_id) )
        {
            // If we get a specific request, return just that, otherwise return
            //  the entire session_data hash.
            if ( !empty($var) )
            {
                return $this->_session_data[$var];
            }
            else
            {
                return $this->_session_data;
            }
        }
        else
        {
            // We don't have a valid session, bail!
            return '';
        }
    }


    /**
     * External method call to set new session variable(s)
     *
     * Always use this function to set session variables in case the
     * internal class variable or structure ever changes. If this function
     * wasn't passed an array with at least one element this function will
     * return FALSE.
     *
     * @param array $vars Array of session variables to be set
     * @return boolean TRUE|FALSE
     */
    function set_session_var($vars=array())
    {
        if ( is_array($vars) and (count($vars) > 0) and !empty($this->_session_id) )
        {
            foreach ( $vars as $k => $v )
            {
                $this->_session_data[$k] = $v;
            }

            $this->r_write_session();

            return TRUE;
        }
        else
        {
            // Either $vars was not an array, it was empty, or we don't
            //  have a valid session, bail!
            return FALSE;
        }
    }


    /**
     * Update the session time, if it hasn't already been
     *
     * @return boolean TRUE|FALSE
     */
    function update_session_time()
    {
        if ( !empty($this->_session_id) and ($this->_time_updated != 1) )
        {
            // Update the current session time
            $this->ipsclass->DB->simple_construct( array( 'update' => 'registry_sessions',
                                                          'set'    => "ses_time='".$this->_time_now."'",
                                                          'where'  => "ses_id='".$this->_session_id."'",
                                                 )      );

            $this->ipsclass->DB->simple_exec();

            if ( $this->ipsclass->DB->get_affected_rows() > 0 )
            {
                return TRUE;
            }
            else
            {
                return FALSE;
            }
        }

        return TRUE;
    }

}

?>